<?php
// $Id: class.tar.php 3575 2009-09-05 19:35:11Z trabis $

/**
 * package::i.tools
 *
 * php-downloader	v1.0	-	www.ipunkt.biz
 *
 * (c)	2002 - www.ipunkt.biz (rok)
 */
/**
 * =======================================================================
 * Name:
 * tar Class
 *
 * Author:
 * Josh Barger <joshb@npt.com>
 *
 * Description:
 * This class reads and writes Tape-Archive (TAR) Files and Gzip
 * compressed TAR files, which are mainly used on UNIX systems.
 * This class works on both windows AND unix systems, and does
 * NOT rely on external applications!! Woohoo!
 *
 * Usage:
 * Copyright (C) 2002  Josh Barger
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details at:
 * http://www.gnu.org/copyleft/lesser.html
 *
 * If you use this script in your application/website, please
 * send me an e-mail letting me know about it :)
 *
 * Bugs:
 * Please report any bugs you might find to my e-mail address
 * at joshb@npt.com.  If you have already created a fix/patch
 * for the bug, please do send it to me so I can incorporate it into my release.
 *
 * Version History:
 * 1.0	04/10/2002	- InitialRelease
 *
 * 2.0	04/11/2002	- Merged both tarReader and tarWriter
 * classes into one
 * - Added support for gzipped tar files
 * Remember to name for .tar.gz or .tgz
 * if you use gzip compression!
 * :: THIS REQUIRES ZLIB EXTENSION ::
 * - Added additional comments to
 * functions to help users
 * - Added ability to remove files and
 * directories from archive
 * 2.1	04/12/2002	- Fixed serious bug in generating tar
 * - Created another example file
 * - Added check to make sure ZLIB is
 * installed before running GZIP
 * compression on TAR
 * 2.2	05/07/2002	- Added automatic detection of Gzipped
 * tar files (Thanks go to Jidgen Falch
 * for the idea)
 * - Changed "private" functions to have
 * special function names beginning with
 * two underscores
 * =======================================================================
 * XOOPS changes onokazu <webmaster@xoops.org>
 *
 * 12/25/2002 - Added flag to addFile() function for binary files
 *
 * =======================================================================
 */

/**
 * tar Class
 *
 * This class reads and writes Tape-Archive (TAR) Files and Gzip
 * compressed TAR files, which are mainly used on UNIX systems.
 * This class works on both windows AND unix systems, and does
 * NOT rely on external applications!! Woohoo!
 *
 * @author Josh Barger <joshb@npt.com>
 * @copyright Copyright (C) 2002  Josh Barger
 * @package kernel
 * @subpackage core
 */

class tar
{
    /**
     * *#@+
     * Unprocessed Archive Information
     */
    var $filename;
    var $isGzipped;
    var $tar_file;
    /**
     * *#@-
     */

    /**
     * *#@+
     * Processed Archive Information
     */
    var $files;
    var $directories;
    var $numFiles;
    var $numDirectories;
    /**
     * *#@-
     */

    /**
     * Class Constructor -- Does nothing...
     */
    function tar()
    {
        return true;
    }

    /**
     * Computes the unsigned Checksum of a file's header
     * to try to ensure valid file
     *
     * @param string $bytestring
     * @access private
     */
    function __computeUnsignedChecksum($bytestring)
    {
        $unsigned_chksum = '';
        for ($i = 0; $i < 512; $i++) {
            $unsigned_chksum += ord($bytestring[$i]);
        }
        for ($i = 0; $i < 8; $i++) {
            $unsigned_chksum -= ord($bytestring[148 + $i]);
            $unsigned_chksum += ord(' ') * 8;
        }
        return $unsigned_chksum;
    }

    /**
     * Converts a NULL padded string to a non-NULL padded string
     *
     * @param string $string
     * @return string
     * @access private
     */
    function __parseNullPaddedString($string)
    {
        $position = strpos($string, chr(0));
        return substr($string, 0, $position);
    }

    /**
     * This function parses the current TAR file
     *
     * @return bool always TRUE
     * @access private
     */
    function __parseTar()
    {
        // Read Files from archive
        $tar_length = strlen($this->tar_file);
        $main_offset = 0;
        $this->numFiles = 0;
        while ($main_offset < $tar_length) {
            // If we read a block of 512 nulls, we are at the end of the archive
            if (substr($this->tar_file, $main_offset, 512) == str_repeat(chr(0), 512))
                break;
                // Parse file name
            $file_name = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset, 100));
            // Parse the file mode
            $file_mode = substr($this->tar_file, $main_offset + 100, 8);
            // Parse the file user ID
            $file_uid = octdec(substr($this->tar_file, $main_offset + 108, 8));
            // Parse the file group ID
            $file_gid = octdec(substr($this->tar_file, $main_offset + 116, 8));
            // Parse the file size
            $file_size = octdec(substr($this->tar_file, $main_offset + 124, 12));
            // Parse the file update time - unix timestamp format
            $file_time = octdec(substr($this->tar_file, $main_offset + 136, 12));
            // Parse Checksum
            $file_chksum = octdec(substr($this->tar_file, $main_offset + 148, 6));
            // Parse user name
            $file_uname = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset + 265, 32));
            // Parse Group name
            $file_gname = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset + 297, 32));
            // Make sure our file is valid
            if ($this->__computeUnsignedChecksum(substr($this->tar_file, $main_offset, 512)) != $file_chksum)
                return false;
                // Parse File Contents
            $file_contents = substr($this->tar_file, $main_offset + 512, $file_size);

            /**
             * ### Unused Header Information ###
             * $activeFile["typeflag"]		= substr($this->tar_file,$main_offset + 156,1);
             * $activeFile["linkname"]		= substr($this->tar_file,$main_offset + 157,100);
             * $activeFile["magic"]		= substr($this->tar_file,$main_offset + 257,6);
             * $activeFile["version"]		= substr($this->tar_file,$main_offset + 263,2);
             * $activeFile["devmajor"]		= substr($this->tar_file,$main_offset + 329,8);
             * $activeFile["devminor"]		= substr($this->tar_file,$main_offset + 337,8);
             * $activeFile["prefix"]		= substr($this->tar_file,$main_offset + 345,155);
             * $activeFile["endheader"]	= substr($this->tar_file,$main_offset + 500,12);
             */

            if ($file_size > 0) {
                // Increment number of files
                $this->numFiles++;
                // Create us a new file in our array
                $activeFile =& $this->files[];
                // Asign Values
                $activeFile["name"] = $file_name;
                $activeFile["mode"] = $file_mode;
                $activeFile["size"] = $file_size;
                $activeFile["time"] = $file_time;
                $activeFile["user_id"] = $file_uid;
                $activeFile["group_id"] = $file_gid;
                $activeFile["user_name"] = $file_uname;
                $activeFile["group_name"] = $file_gname;
                $activeFile["checksum"] = $file_chksum;
                $activeFile["file"] = $file_contents;
            } else {
                // Increment number of directories
                $this->numDirectories ++;
                // Create a new directory in our array
                $activeDir =& $this->directories[];
                // Assign values
                $activeDir["name"] = $file_name;
                $activeDir["mode"] = $file_mode;
                $activeDir["time"] = $file_time;
                $activeDir["user_id"] = $file_uid;
                $activeDir["group_id"] = $file_gid;
                $activeDir["user_name"] = $file_uname;
                $activeDir["group_name"] = $file_gname;
                $activeDir["checksum"] = $file_chksum;
            }
            // Move our offset the number of blocks we have processed
            $main_offset += 512 + (ceil($file_size / 512) * 512);
        }

        return true;
    }

    /**
     * Read a non gzipped tar file in for processing.
     *
     * @param string $filename full filename
     * @return bool always TRUE
     * @access private
     */
    function __readTar($filename = '')
    {
        // Set the filename to load
        if (!$filename) {
            $filename = $this->filename;
        }
        // Read in the TAR file
        $fp = fopen($filename, 'rb');
        $this->tar_file = fread($fp, filesize($filename));
        fclose($fp);

        if ($this->tar_file[0] == chr(31) && $this->tar_file[1] == chr(139) && $this->tar_file[2] == chr(8)) {
            if (!function_exists('gzinflate')) {
                return false;
            }
            $this->isGzipped = true;
            $this->tar_file = gzinflate(substr($this->tar_file, 10, - 4));
        }
        // Parse the TAR file
        $this->__parseTar();

        return true;
    }

    /**
     * Generates a TAR file from the processed data
     *
     * @return bool always TRUE
     * @access private
     */
    function __generateTAR()
    {
        // Clear any data currently in $this->tar_file
        unset($this->tar_file);
        // Generate Records for each directory, if we have directories
        if ($this->numDirectories > 0) {
            foreach ($this->directories as $key => $information) {
                unset($header);
                // Generate tar header for this directory
                // Filename, Permissions, UID, GID, size, Time, checksum, typeflag, linkname, magic, version, user name, group name, devmajor, devminor, prefix, end
                $header .= str_pad($information["name"], 100, chr(0));
                $header .= str_pad(decoct($information["mode"]), 7, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_pad(decoct($information["user_id"]), 7, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_pad(decoct($information["group_id"]), 7, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_pad(decoct(0), 11, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_pad(decoct($information["time"]), 11, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_repeat(" ", 8);
                $header .= "5";
                $header .= str_repeat(chr(0), 100);
                $header .= str_pad("ustar", 6, chr(32));
                $header .= chr(32) . chr(0);
                $header .= str_pad("", 32, chr(0));
                $header .= str_pad("", 32, chr(0));
                $header .= str_repeat(chr(0), 8);
                $header .= str_repeat(chr(0), 8);
                $header .= str_repeat(chr(0), 155);
                $header .= str_repeat(chr(0), 12);
                // Compute header checksum
                $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT);
                for ($i = 0; $i < 6; $i ++) {
                    $header[(148 + $i)] = substr($checksum, $i, 1);
                }
                $header[154] = chr(0);
                $header[155] = chr(32);
                // Add new tar formatted data to tar file contents
                $this->tar_file .= $header;
            }
        }
        // Generate Records for each file, if we have files (We should...)
        if ($this->numFiles > 0) {
            $this->tar_file = '';
            foreach($this->files as $key => $information) {
                unset($header);
                // Generate the TAR header for this file
                // Filename, Permissions, UID, GID, size, Time, checksum, typeflag, linkname, magic, version, user name, group name, devmajor, devminor, prefix, end
                $header = str_pad($information["name"], 100, chr(0));
                $header .= str_pad(decoct($information["mode"]), 7, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_pad(decoct($information["user_id"]), 7, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_pad(decoct($information["group_id"]), 7, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_pad(decoct($information["size"]), 11, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_pad(decoct($information["time"]), 11, "0", STR_PAD_LEFT) . chr(0);
                $header .= str_repeat(" ", 8);
                $header .= "0";
                $header .= str_repeat(chr(0), 100);
                $header .= str_pad("ustar", 6, chr(32));
                $header .= chr(32) . chr(0);
                $header .= str_pad($information["user_name"], 32, chr(0)); // How do I get a file's user name from PHP?
                $header .= str_pad($information["group_name"], 32, chr(0)); // How do I get a file's group name from PHP?
                $header .= str_repeat(chr(0), 8);
                $header .= str_repeat(chr(0), 8);
                $header .= str_repeat(chr(0), 155);
                $header .= str_repeat(chr(0), 12);
                // Compute header checksum
                $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT);
                for ($i = 0; $i < 6; $i++) {
                    $header[(148 + $i)] = substr($checksum, $i, 1);
                }
                $header[154] = chr(0);
                $header[155] = chr(32);
                // Pad file contents to byte count divisible by 512
                $file_contents = str_pad($information["file"], (ceil($information["size"] / 512) * 512), chr(0));
                // Add new tar formatted data to tar file contents
                $this->tar_file .= $header . $file_contents;
            }
        }
        // Add 512 bytes of NULLs to designate EOF
        $this->tar_file .= str_repeat(chr(0), 512);
        return true;
    }

    /**
     * Open a TAR file
     *
     * @param string $filename
     * @return bool
     */
    function openTAR($filename)
    {
        // Clear any values from previous tar archives
        unset($this->filename);
        unset($this->isGzipped);
        unset($this->tar_file);
        unset($this->files);
        unset($this->directories);
        unset($this->numFiles);
        unset($this->numDirectories);
        // If the tar file doesn't exist...
        if (!file_exists($filename))
            return false;

        $this->filename = $filename;
        // Parse this file
        $this->__readTar();
        return true;
    }

    /**
     * Appends a tar file to the end of the currently opened tar file.
     *
     * @param string $filename
     * @return bool
     */
    function appendTar($filename)
    {
        // If the tar file doesn't exist...
        if (!file_exists($filename)) {
            return false;
        }
        $this->__readTar($filename);
        return true;
    }

    /**
     * Retrieves information about a file in the current tar archive
     *
     * @param string $filename
     * @return string FALSE on fail
     */
    function getFile($filename)
    {
        if ($this->numFiles > 0) {
            foreach ($this->files as $key => $information) {
                if ($information['name'] == $filename) {
                    return $information;
                }
            }
        }
        return false;
    }

    /**
     * Retrieves information about a directory in the current tar archive
     *
     * @param string $dirname
     * @return string FALSE on fail
     */
    function getDirectory($dirname)
    {
        if ($this->numDirectories > 0) {
            foreach ($this->directories as $key => $information) {
                if ($information['name'] == $dirname) {
                    return $information;
                }
            }
        }
        return false;
    }

    /**
     * Check if this tar archive contains a specific file
     *
     * @param string $filename
     * @return bool
     */
    function containsFile($filename)
    {
        if ($this->numFiles > 0) {
            foreach ($this->files as $key => $information) {
                if ($information['name'] == $filename) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Check if this tar archive contains a specific directory
     *
     * @param string $dirname
     * @return bool
     */
    function containsDirectory($dirname)
    {
        if ($this->numDirectories > 0) {
            foreach ($this->directories as $key => $information) {
                if ($information['name'] == $dirname) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Add a directory to this tar archive
     *
     * @param string $dirname
     * @return bool
     */
    function addDirectory($dirname)
    {
        if (! file_exists($dirname)) {
            return false;
        }
        // Get directory information
        $file_information = stat($dirname);
        // Add directory to processed data
        $this->numDirectories++;
        $activeDir =& $this->directories[];
        $activeDir['name'] = $dirname;
        $activeDir['mode'] = $file_information['mode'];
        $activeDir['time'] = $file_information['time'];
        $activeDir['user_id'] = $file_information['uid'];
        $activeDir['group_id'] = $file_information['gid'];
        $activeDir['checksum'] = $checksum;

        return true;
    }

    /**
     * Add a file to the tar archive
     *
     * @param string $filename
     * @param boolean $binary Binary file?
     * @return bool
     */
    function addFile($filename, $binary = false)
    {
        // Make sure the file we are adding exists!
        if (!file_exists($filename)) {
            return false;
        }
        // Make sure there are no other files in the archive that have this same filename
        if ($this->containsFile($filename)) {
            return false;
        }
        // Get file information
        $file_information = stat($filename);
        // Read in the file's contents
        if (!$binary) {
            $fp = fopen($filename, 'r');
        } else {
            $fp = fopen($filename, 'rb');
        }
        $file_contents = fread($fp, filesize($filename));
        fclose($fp);
        // Add file to processed data
        $this->numFiles++;
        $activeFile =& $this->files[];
        $activeFile['name'] = $filename;
        $activeFile['mode'] = $file_information['mode'];
        $activeFile['user_id'] = $file_information['uid'];
        $activeFile['group_id'] = $file_information['gid'];
        $activeFile['size'] = $file_information['size'];
        $activeFile['time'] = $file_information['mtime'];
        $activeFile['checksum'] = isset($checksum) ? $checksum : '';
        $activeFile['user_name'] = '';
        $activeFile['group_name'] = '';
        $activeFile['file'] = trim($file_contents);
        return true;
    }

    /**
     * Remove a file from the tar archive
     *
     * @param string $filename
     * @return bool
     */
    function removeFile($filename)
    {
        if ($this->numFiles > 0) {
            foreach ($this->files as $key => $information) {
                if ($information['name'] == $filename) {
                    $this->numFiles--;
                    unset($this->files[$key]);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Remove a directory from the tar archive
     *
     * @param string $dirname
     * @return bool
     */
    function removeDirectory($dirname)
    {
        if ($this->numDirectories > 0) {
            foreach ($this->directories as $key => $information) {
                if ($information['name'] == $dirname) {
                    $this->numDirectories--;
                    unset($this->directories[$key]);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Write the currently loaded tar archive to disk
     *
     * @return bool
     */
    function saveTar()
    {
        if (!$this->filename) {
            return false;
        }
        // Write tar to current file using specified gzip compression
        $this->toTar($this->filename, $this->isGzipped);
        return true;
    }

    /**
     * Saves tar archive to a different file than the current file
     *
     * @param string $filename
     * @param bool $useGzip Use GZ compression?
     * @return bool
     */
    function toTar($filename, $useGzip)
    {
        if (!$filename) {
            return false;
        }
        // Encode processed files into TAR file format
        $this->__generateTar();
        // GZ Compress the data if we need to
        if ($useGzip) {
            // Make sure we have gzip support
            if (!function_exists('gzencode')) {
                return false;
            }
            $file = gzencode($this->tar_file);
        } else {
            $file = $this->tar_file;
        }
        // Write the TAR file
        $fp = fopen($filename, 'wb');
        fwrite($fp, $file);
        fclose($fp);
        return true;
    }

    /**
     * Sends tar archive to stdout
     *
     * @param string $filename
     * @param bool $useGzip Use GZ compression?
     * @return string
     */
    function toTarOutput($filename, $useGzip)
    {
        if (!$filename) {
            return false;
        }
        // Encode processed files into TAR file format
        $this->__generateTar();
        // GZ Compress the data if we need to
        if ($useGzip) {
            // Make sure we have gzip support
            if (!function_exists('gzencode')) {
                return false;
            }
            $file = gzencode($this->tar_file);
        } else {
            $file = $this->tar_file;
        }
        return $file;
    }
}

?>